跳到主要内容

LangChain RAG 实践

目标:用 LangChain 在同一小文档库上跑通 ingest → 检索 → 生成,与 RAG 落地实践 的无框架 PoC 逐步对照。概念见 RAG 与检索增强。

本篇只覆盖 RAG 链路;Agent 图编排见 [LangGraph 编排实践](/docs/ai/Agent/LangGraph 编排实践)。

目标与验收​

验收项标准
ingest2~3 个 .txt 经 Loader + Splitter 写入 Chroma
检索retriever.invoke(query) 返回相关 Document
生成Prompt 中可见引用片段;无命中时拒答
对照能说出与无框架 PoC 各阶段的对应关系

建议目录​

langchain_rag/
data/ # 与 mini_rag 相同的几篇短文
ingest.py
query.py
requirements.txt

requirements.txt 示例(版本以 LangChain 文档 为准):

langchain-core
langchain-community
langchain-text-splitters
langchain-chroma
chromadb

流水线映射​

RAG 落地实践LangChain 组件
chunk_textRecursiveCharacterTextSplitter
build_index 列表DocumentLoader + vector_store.add_documents
score / retrievevector_store.as_retriever
answer 模板Prompt 模板 + LLM(或模板拼接演示)

切分与入库​

# ingest.py
from pathlib import Path

from langchain_community.document_loaders import TextLoader
from langchain_community.embeddings import FakeEmbeddings
from langchain_community.vectorstores import Chroma
from langchain_text_splitters import RecursiveCharacterTextSplitter

DATA_DIR = "data"
PERSIST_DIR = "chroma_db"

def ingest():
docs = []
for path in Path(DATA_DIR).glob("*.txt"):
docs.extend(TextLoader(str(path), encoding="utf-8").load())

splitter = RecursiveCharacterTextSplitter(
chunk_size=200, chunk_overlap=40, add_start_index=True
)
splits = splitter.split_documents(docs)

# PoC 用 FakeEmbeddings,无需 API;上线换真实 Embeddings 模型
embeddings = FakeEmbeddings(size=128)
vector_store = Chroma.from_documents(
documents=splits,
embedding=embeddings,
persist_directory=PERSIST_DIR,
)
return vector_store

if __name__ == "__main__":
ingest()
print("indexed")

FakeEmbeddings 仅用于本地演示向量库流程;语义检索效果弱,换 OpenAIEmbeddings / 本地 HuggingFaceEmbeddings 后行为才接近生产。

检索​

# query.py(片段)
def get_retriever(vector_store, k: int = 3):
return vector_store.as_retriever(
search_type="similarity",
search_kwargs={"k": k},
)

def retrieve_docs(retriever, query: str, score_threshold: float | None = None):
docs = retriever.invoke(query)
if not docs:
return []
# 可选:Chroma 带 score 的 similarity_search_with_score 做阈值拒答
return docs

加载已有索引:

from langchain_community.embeddings import FakeEmbeddings
from langchain_community.vectorstores import Chroma

embeddings = FakeEmbeddings(size=128)
vector_store = Chroma(persist_directory=PERSIST_DIR, embedding_function=embeddings)
retriever = get_retriever(vector_store, k=3)

生成(模板 + 拒答)​

无 LLM API 时,与无框架版同样用模板拼接;有 API 时把 context 塞进 Prompt 调模型即可。

def answer(query: str, docs: list) -> str:
if not docs:
return "知识库中未找到依据,无法回答。"
context = "\n---\n".join(
f"[{d.metadata.get('source', '?')}] {d.page_content}" for d in docs
)
return (
f"依据如下片段作答:\n{context}\n\n"
f"(此处可替换为 ChatOpenAI / 其他 LLM)\n问题:{query}"
)

# 主流程
# hits = retrieve_docs(retriever, "退货几天内可以?")
# print(answer("退货几天内可以?", hits))

接真 LLM 时常见写法(示意,import 路径以当前文档为准):

from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser

prompt = ChatPromptTemplate.from_messages([
("system", "仅根据下列片段回答;无依据则说找不到。\n\n{context}"),
("human", "{question}"),
])
# chain = prompt | llm | StrOutputParser()

调参直觉​

改动与无框架 PoC 相同现象
chunk_size 变大单段更全,噪声与定位变差
chunk_overlap 变大边界语义更连贯,冗余与成本上升
k 变大更易召回,也更易掺无关段

用 RAG 落地实践 里的 eval_set.jsonl 跑同一批问题,对比两种实现的 must_include 命中率。

常见坑​

  1. 空库仍生成:检索无 Document 时必须拒答,不要直接调 LLM。
  2. Embedding 不一致:ingest 与 query 必须用同一 embedding 模型(含维度)。
  3. 中文与词袋:即使用向量,小模型对专有名词仍可能弱——引出混合检索 / 重排(见概念篇 Advanced 表)。
  4. API 漂移:LangChain 包拆分频繁(langchain-community 等),以官方文档 import 为准。

实践要点​

  1. 先跑通 RAG 落地实践 无框架版,再对照本篇组件,理解更快。
  2. Loader → Splitter → VectorStore → Retriever 四步即工业界 RAG 主干,Agent 只是在之上加规划循环。
  3. PoC 用 FakeEmbeddings + 模板生成;评测通过后再换真实 embedding 与 LLM。
  4. 向量 RAG 搞不定的全局归纳见 [GraphRAG 概览与查询](/docs/ai/RAG/GraphRAG 概览与查询)。
  5. 生产向量库见 [Milvus 向量库实践](/docs/ai/RAG/Milvus 向量库实践)(从 Chroma 迁移时通常只换 VectorStore)。
  6. 面试挂钩:能画 LangChain RAG 四组件,并说明与 Naive 流水线的一一对应即可。